package com.rati.usercenter.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rati.usercenter.exception.BusinessException;
import com.rati.usercenter.mapper.TeamMapper;
import com.rati.usercenter.mapper.UserTeamMapper;
import com.rati.usercenter.model.DTO.TeamInfoDTO;
import com.rati.usercenter.model.VO.TeamVO;
import com.rati.usercenter.model.VO.UserVO;
import com.rati.usercenter.model.domain.Team;
import com.rati.usercenter.model.domain.User;
import com.rati.usercenter.model.domain.UserTeam;
import com.rati.usercenter.model.enums.TeamStatusEnum;
import com.rati.usercenter.model.request.TeamQuery;
import com.rati.usercenter.service.TeamService;
import com.rati.usercenter.service.UserService;
import com.rati.usercenter.service.UserTeamService;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.DigestUtils;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static com.rati.usercenter.common.ErrorCode.*;
import static com.rati.usercenter.model.enums.TeamStatusEnum.valueOf;
import static com.rati.usercenter.model.enums.TeamStatusEnum.*;

/**
 * @author Lwh
 * @description 针对表【team(队伍)】的数据库操作Service实现
 * @createDate 2022-12-23 12:32:35
 */
@Service
public class TeamServiceImpl extends ServiceImpl<TeamMapper, Team>
        implements TeamService {

    @Resource
    private UserService userService;

    @Resource
    private UserTeamService userTeamService;

    @Resource
    private UserTeamMapper userTeamMapper;

    @Resource
    private TeamMapper teamMapper;

    @Resource
    private RedissonClient redissonClient;

    @Resource
    private RedisTemplate<String, List<UserVO>> redisTemplate;

    private final String keyPrefix = "usercenter:team:listTeam:%s";

    /**
     * 创建队伍
     *
     * @param team      队伍信息
     * @param loginUser 登录用户
     * @return 新队伍id
     */
    @Override
    @Transactional(rollbackFor = RuntimeException.class)
    public long createTeam(Team team, UserVO loginUser) {
//      1. 请求参数是否为空？
        if (team == null) {
            throw new BusinessException(PARAMS_ERROR, "请求参数为空");
        }
//      2. 校验信息
//          1. 队伍人数 > 1 且 <= 20
        int maxNum = Optional.ofNullable(team.getMaxNum()).orElse(0);
        if (maxNum < 1 || maxNum > 20) {
            throw new BusinessException(PARAMS_ERROR, "队伍人数设置不合适");
        }
//          2. 队伍标题 <= 20
        String teamName = team.getTeamName();
        if (StringUtils.isBlank(teamName) || teamName.length() > 20) {
            throw new BusinessException(PARAMS_ERROR, "队伍名称过长");
        }
//          3. 描述 <= 512
        String description = team.getDescription();
        if (StringUtils.isBlank(description) || description.length() > 512) {
            throw new BusinessException(PARAMS_ERROR, "队伍描述过长");
        }
//          4. status 是否公开（int）不传默认为 0（公开）
        Integer status = Optional.ofNullable(team.getStatus()).orElse(0);
        TeamStatusEnum statusEnum = TeamStatusEnum.valueOf(status);
        if (statusEnum == null) {
            throw new BusinessException(PARAMS_ERROR, "队伍状态错误");
        }
//          5. 如果 status 是加密状态，一定要有密码，且密码 <= 32
        String password = team.getPassword();
        if (Objects.equals(statusEnum, TeamStatusEnum.ENCRYPTED)) {
            if (StringUtils.isBlank(password) || password.length() > 32) {
                throw new BusinessException(PARAMS_ERROR, "密码设置不合适");
            }
            String passwordE = DigestUtils.md5DigestAsHex(password.getBytes());
            team.setPassword(passwordE);
        }
//          6. 超时时间 > 当前时间
        LocalDateTime dateTime = Optional.ofNullable(team.getExpireTime()).orElse(LocalDateTime.MAX);
        if (dateTime.isBefore(LocalDateTime.now())) {
            throw new BusinessException(PARAMS_ERROR, "过期时间设置不合理");
        }
//          7. 校验用户最多创建 5 个队伍
        RLock lock = redissonClient.getLock("usercenter:team:createTeam:lock");
        try {
            if (lock.tryLock(5, -1, TimeUnit.SECONDS)) {
                QueryWrapper<Team> wrapper = new QueryWrapper<>();
                wrapper.eq("create_user_id", loginUser.getId());
                long hasTeamNum = this.count(wrapper);
                if (hasTeamNum >= 5) {
                    throw new BusinessException(NO_AUTH, "不能创建过多队伍");
                }
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
//      3. 插入队伍信息到队伍表
        team.setId(null);
        boolean result = this.save(team);
        Long teamId = team.getId();
        if (!result || teamId == null) {
            throw new BusinessException(SERVER_ERROR, "队伍信息保存失败");
        }
//      4. 插入用户  => 队伍关系到关系表
        UserTeam userTeam = new UserTeam();
        userTeam.setUserId(loginUser.getId());
        userTeam.setTeamId(teamId);
        userTeam.setJoinTime(LocalDateTime.now());
        result = userTeamService.save(userTeam);
        if (!result) {
            throw new BusinessException(SERVER_ERROR, "用户队伍关系保存失败");
        }
        return teamId;
    }

    /**
     * 删除队伍
     *
     * @param id          被删除队伍id
     * @param currentUser 当前登录用户
     */
    @Override
    @Transactional(rollbackFor = RuntimeException.class)
    public void deleteTeam(long id, UserVO currentUser) {
//      1. 校验请求参数
        if (id <= 0) {
            throw new BusinessException(PARAMS_ERROR, "请求参数非法");
        }
//      2. 校验队伍是否存在
        Team oldTeam = this.getById(id);
        if (oldTeam == null) {
            throw new BusinessException(NULL_ERROR, "该队伍不存在");
        }
//      3. 校验你是不是队伍的队长
        if (!Objects.equals(oldTeam.getCreateUserId(), currentUser.getId()) && userService.isNotAdmin(currentUser)) {
            throw new BusinessException(NO_AUTH, "该用户无权删除该队伍");
        }
//      4. 移除所有加入队伍的关联信息
        QueryWrapper<UserTeam> wrapper = new QueryWrapper<>();
        wrapper.eq("team_id", id);
        boolean result = userTeamService.remove(wrapper);
        if (!result) {
            throw new BusinessException(SERVER_ERROR, "删除用户队伍关联信息失败");
        }
//      5. 删除队伍
        result = this.removeById(id);
        if (!result) {
            throw new BusinessException(SERVER_ERROR, "删除队伍失败");
        }
    }

    /**
     * 更新队伍信息
     *
     * @param teamInfoDTO 新队伍信息
     * @param currentUser 当前登录用户
     */
    @Override
    public void updateTeam(TeamInfoDTO teamInfoDTO, UserVO currentUser) {
//        2. 查询队伍是否存在
        Team oldTeam = this.getById(teamInfoDTO.getId());
        if (oldTeam == null) {
            throw new BusinessException(NULL_ERROR, "该队伍不存在");
        }
        Team newTeam = new Team();
        BeanUtils.copyProperties(oldTeam, newTeam);
        copyAndSetTime(teamInfoDTO, newTeam);
//        3. 只有管理员或者队伍的创建者可以修改
        if (!Objects.equals(oldTeam.getCreateUserId(), currentUser.getId()) && userService.isNotAdmin(currentUser)) {
            throw new BusinessException(NO_AUTH, "该用户无权修改该队伍");
        }
//        4. 如果队伍状态改为加密，必须要有密码
        TeamStatusEnum oldStatus = valueOf(oldTeam.getStatus());
        TeamStatusEnum newStatus = Optional.ofNullable(valueOf(newTeam.getStatus())).orElse(PUBLIC);
        if (Objects.equals(newStatus, ENCRYPTED)) {
            if (!Objects.equals(oldStatus, ENCRYPTED)) {
                if (StringUtils.isBlank(newTeam.getPassword())) {
                    throw new BusinessException(PARAMS_ERROR, "必须输入密码");
                }
            }
        } else {
            newTeam.setPassword(null);
        }
//        5. 更新成功
        boolean result = this.updateById(newTeam);
        if (!result) {
            throw new BusinessException(SERVER_ERROR, "队伍信息更新失败");
        }
    }

    /**
     * 根据条件查询队伍
     *
     * @param teamQuery   查询的队伍信息
     * @param currentUser 当前登录的用户
     * @return 队伍列表
     */
    @Override
    public List<TeamVO> listTeam(TeamQuery teamQuery, UserVO currentUser) {
//         1. 从请求参数中取出队伍名称等查询条件，如果存在则作为查询条件
        QueryWrapper<Team> wrapper = new QueryWrapper<>();
//         3. 可以通过某个关键词同时对名称和描述查询
        String text = teamQuery.getText();
        if (StringUtils.isNotBlank(text)) {
            wrapper.like("team_name", text).or().like("description", text);
        }
        Integer maxNum = teamQuery.getMaxNum();
        if (maxNum != null) {
            if (maxNum < 1) {
                throw new BusinessException(PARAMS_ERROR, "查询参数非法");
            }
            wrapper.eq("max_num", maxNum);
        }
        Long userId = teamQuery.getCreateUserId();
        if (userId != null) {
            if (userId < 1) {
                throw new BusinessException(PARAMS_ERROR, "查询参数非法");
            }
            wrapper.eq("create_user_id", userId);
        }
//         4. 只有管理员才能查看加密还有非公开的房间
        TeamStatusEnum status = valueOf(teamQuery.getStatus());
        if (status != null) {
            if (!status.equals(PUBLIC) && userService.isNotAdmin(currentUser)) {
                throw new BusinessException(NO_AUTH, "无权查询该类队伍");
            } else {
                wrapper.eq("status", status.getValue());
            }
        } else {
            wrapper.eq("status", PUBLIC.getValue());
        }
//         2. 不展示已过期的队伍（根据过期时间筛选）
        wrapper.gt("expire_time", LocalDateTime.now());
//        分页查询
        int pageNum = teamQuery.getPageNum();
        int pageSize = teamQuery.getPageSize();
        List<Team> teamList = this.page(new Page<>(pageNum, pageSize), wrapper).getRecords();
        if (teamList.isEmpty()) {
            throw new BusinessException(NULL_ERROR, "无符合条件的队伍");
        }
        return getTeamVOList(teamList);
    }

    /**
     * 根据用户id获取其创建的队伍
     *
     * @param userId 用户id
     * @return 该用户创建的队伍列表
     */
    @Override
    public List<TeamVO> getCreateTeam(Long userId) {
        if (userId < 1) {
            throw new BusinessException(PARAMS_ERROR, "请求参数非法");
        }
        QueryWrapper<Team> wrapper = new QueryWrapper<>();
        wrapper.eq("create_user_id", userId);
        List<Team> teamList = this.list(wrapper);
        if (teamList.isEmpty()) {
            throw new BusinessException(NULL_ERROR, "该用户未创建队伍");
        }
        return getTeamVOList(teamList);
    }

    /**
     * 根据用户id获取其加入的队伍
     *
     * @param userId 用户id
     * @return 该用户加入的队伍列表
     */
    @Override
    public List<TeamVO> getJoinedTeam(Long userId) {
        if (userId < 1) {
            throw new BusinessException(PARAMS_ERROR, "请求参数非法");
        }
        List<Team> teamList = userTeamMapper.getJoinTeamByUserId(userId);
        if (teamList.isEmpty()) {
            throw new BusinessException(NULL_ERROR, "该用户未加入队伍");
        }
        return getTeamVOList(teamList);
    }

    /**
     * 关联查询已加入队伍的用户信息
     *
     * @param teamList 原队伍列表
     * @return 可展示的队伍信息列表
     */
    @NotNull
    private List<TeamVO> getTeamVOList(List<Team> teamList) {
        List<TeamVO> teamVOList = new ArrayList<>();
        for (Team team : teamList) {
            TeamVO teamVO = new TeamVO();
            BeanUtils.copyProperties(team, teamVO);
//         5. 关联查询已加入队伍的用户信息
//            先查缓存
            String key = String.format(keyPrefix, teamVO.getId());
            ValueOperations<String, List<UserVO>> valueOperations = redisTemplate.opsForValue();
            List<UserVO> memList = valueOperations.get(key);
            if (memList != null) {
//                缓存命中
                teamVO.setUserList(memList);
            } else {
//                缓存未命中，查数据库
                List<User> members = userTeamMapper.getMembersByTeamId(teamVO.getId());
                memList = members.stream().map(userService::getSafetyUser).collect(Collectors.toList());
                teamVO.setUserList(memList);
//                写缓存
                valueOperations.set(key, memList, 10, TimeUnit.MINUTES);
            }
            teamVOList.add(teamVO);
        }
        return teamVOList;
    }

    /**
     * 用户通过id加入队伍
     *
     * @param teamId      队伍id
     * @param password    密码（可选）
     * @param currentUser 当前登录用户
     */
    @Override
    public void joinTeamById(long teamId, String password, UserVO currentUser) {
        if (teamId < 1) {
            throw new BusinessException(PARAMS_ERROR, "请求参数非法");
        }
        Team team = this.getById(teamId);
        Long userId = currentUser.getId();
//        队伍必须存在、未过期
        if (team == null) {
            throw new BusinessException(NULL_ERROR, "该队伍不存在");
        }
        if (team.getExpireTime().isBefore(LocalDateTime.now())) {
            throw new BusinessException(SERVER_ERROR, "队伍已过期");
        }
//        禁止加入私有的队伍
        if (Objects.equals(team.getStatus(), PRIVATE.getValue())) {
            throw new BusinessException(NO_AUTH, "不能加入私有队伍");
        }
//        如果加入的队伍是加密的，密码必须匹配
        if (Objects.equals(team.getStatus(), ENCRYPTED.getValue())) {
            String passwordE = DigestUtils.md5DigestAsHex(password.getBytes());
            if (!StringUtils.equals(passwordE, team.getPassword())) {
                throw new BusinessException(PARAMS_ERROR, "密码错误");
            }
        }
        RLock userLock = redissonClient.getLock("usercenter:team:joinTeamById:user:" + userId);
        RLock teamLock = redissonClient.getLock("usercenter:team:joinTeamById:team:" + teamId);
//        先获取用户锁（队伍冲突的情况更频繁）
        while (true) {
            try {
                if (userLock.tryLock(0,-1,TimeUnit.SECONDS)) {
//                    最多加入 5 个队伍（用户锁）
                    QueryWrapper<UserTeam> wrapper = new QueryWrapper<>();
                    wrapper.eq("user_id", userId);
                    long countTeam = userTeamService.count(wrapper);
                    if (countTeam >= 5) {
                        throw new BusinessException(NO_AUTH, "不能加入过多队伍");
                    }
//                    不能重复加入队伍（幂等性）（任意锁）
                    if (isMember(userId, teamId)) {
                        throw new BusinessException(PARAMS_ERROR, "不能重复加入队伍");
                    }
//                    再获取队伍锁
                    while (true) {
                        if (teamLock.tryLock(0,-1,TimeUnit.SECONDS)){
//                            不能加入满员的队伍（队伍锁）
                            if (countMem(teamId) >= team.getMaxNum()) {
                                throw new BusinessException(SERVER_ERROR, "队伍已满员");
                            }
//                            新增队伍 - 用户关联信息
                            UserTeam userTeam = new UserTeam();
                            userTeam.setUserId(userId);
                            userTeam.setTeamId(teamId);
                            userTeam.setJoinTime(LocalDateTime.now());
                            boolean result = userTeamService.save(userTeam);
                            if (!result) {
                                throw new BusinessException(SERVER_ERROR, "用户队伍关联信息插入失败");
                            }
//                            删除缓存中该队伍的记录
                            redisTemplate.delete(String.format(keyPrefix, teamId));
                            break;
                        }
                    }
                    break;
                }
            } catch (InterruptedException e) {
                log.error("joinTeamById error: " + e);
                throw new RuntimeException(e);
            } finally {
                if (teamLock.isHeldByCurrentThread()){
                    teamLock.unlock();
                }
                if (userLock.isHeldByCurrentThread()) {
                    userLock.unlock();
                }
            }
        }
    }

    /**
     * 用户退出队伍
     *
     * @param teamId      队伍id
     * @param currentUser 当前登录用户
     */
    @Override
    @Transactional(rollbackFor = RuntimeException.class)
    public void quitTeam(long teamId, UserVO currentUser) {
//        1. 校验请求参数
        if (teamId < 1) {
            throw new BusinessException(PARAMS_ERROR, "请求参数非法");
        }
//        2. 校验队伍是否存在
        Team team = this.getById(teamId);
        if (team == null) {
            throw new BusinessException(NULL_ERROR, "该队伍不存在");
        }
//        3. 校验我是否已加入队伍
        Long userId = currentUser.getId();
        if (!isMember(userId, teamId)) {
            throw new BusinessException(PARAMS_ERROR, "该用户不是该队伍的成员");
        }
//        处理后事
//        4. 如果队伍
        long countMem = countMem(teamId);
        if (countMem == 1) {
//           1. 只剩一人，队伍解散
            deleteTeam(teamId, currentUser);
        } else {
//           2. 还有其他人
            if (isLeader(userId, teamId)) {
//              1. 如果是队长退出队伍，权限转移给第二早加入的用户 —— 先来后到
//                 > 只用取 id 最小的 2 条数据
                QueryWrapper<UserTeam> wrapper = new QueryWrapper<>();
                wrapper.eq("team_id", teamId);
                wrapper.last("order by id limit 2");
                List<UserTeam> list = userTeamService.list(wrapper);
                Long newLeaderId = list.get(1).getUserId();
                changeLeaderTo(teamId, newLeaderId);
            }
//              2. 非队长，自己退出队伍
        }
//        默默离开
        QueryWrapper<UserTeam> wrapper = new QueryWrapper<>();
        wrapper.eq("user_id", userId);
        wrapper.eq("team_id", teamId);
        boolean result = userTeamService.remove(wrapper);
        if (!result) {
            throw new BusinessException(SERVER_ERROR, "删除用户队伍关系失败");
        }
//        删除缓存中该队伍的记录
        redisTemplate.delete(String.format(keyPrefix, teamId));
    }

    /**
     * 改变队伍队长
     *
     * @param teamId      队伍id
     * @param newLeaderId 新队长id
     */
    @Override
    public void changeLeaderTo(long teamId, Long newLeaderId) {
        if (teamId < 1 || newLeaderId < 1) {
            throw new BusinessException(PARAMS_ERROR, "请求参数非法");
        }
        if (isLeader(newLeaderId, teamId)) {
            throw new BusinessException(PARAMS_ERROR, "该用户已是该队伍队长");
        }
        boolean result = teamMapper.changeLeader(teamId, newLeaderId);
        if (!result) {
            throw new BusinessException(SERVER_ERROR, "修改队长失败");
        }
    }

    /**
     * 根据队伍id获取成员数量
     *
     * @param teamId 队伍id
     * @return 成员数量
     */
    @Override
    public long countMem(long teamId) {
        if (teamId < 1) {
            throw new BusinessException(PARAMS_ERROR, "请求参数非法");
        }
        QueryWrapper<UserTeam> wrapper = new QueryWrapper<>();
        wrapper.eq("team_id", teamId);
        return userTeamService.count(wrapper);
    }

    /**
     * 判断某用户是否是某队伍的队长（创建人）
     *
     * @param userId 用户id
     * @param teamId 队伍id
     * @return 是否是队长
     */
    @Override
    public boolean isLeader(long userId, long teamId) {
        if (userId < 1 || teamId < 1) {
            throw new BusinessException(PARAMS_ERROR, "请求参数非法");
        }
        Team team = this.getById(teamId);
        if (team == null) {
            throw new BusinessException(NULL_ERROR, "该队伍不存在");
        }
        return Objects.equals(userId, team.getCreateUserId());
    }

    /**
     * 判断某用户是否是某队伍的成员
     *
     * @param userId 用户id
     * @param teamId 队伍id
     * @return 是否是成员
     */
    @Override
    public boolean isMember(long userId, long teamId) {
        if (userId < 1 || teamId < 1) {
            throw new BusinessException(PARAMS_ERROR, "请求参数非法");
        }
        QueryWrapper<UserTeam> wrapper = new QueryWrapper<>();
        wrapper.eq("user_id", userId);
        wrapper.eq("team_id", teamId);
        return userTeamService.count(wrapper) > 0;
    }

    /**
     * 复制其他属性和时间属性 String -> LocalDateTime
     */
    @Override
    public void copyAndSetTime(TeamInfoDTO teamInfoDTO, Team team) {
        BeanUtils.copyProperties(teamInfoDTO, team);
        String time = teamInfoDTO.getExpireTime();
        if (time != null) {
            LocalDateTime dateTime = LocalDateTime.parse(time, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
            team.setExpireTime(dateTime);
        }
    }


}




